/*
 * Copyright (c) 2018 Hitevision
 */

package tw.com.hitevision.whiteboard.android.render;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.text.InputType;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import java.util.ArrayDeque;
import java.util.Deque;

import androidx.annotation.ColorInt;
import androidx.core.content.ContextCompat;
import tw.com.hitevision.whiteboard.android.R;
import tw.com.hitevision.whiteboard.android.display.DisplayDpiHelper;
import tw.com.hitevision.whiteboard.android.state.WordProperty;
import tw.com.hitevision.whiteboard.android.stateFile.WordStateFile;
import tw.com.hitevision.whiteboard.android.util.FontSizeCalculator;
import tw.com.hitevision.whiteboard.android.util.TextLayoutUtil;

public class Word extends BaseItem {
    private static final String TAG = Word.class.getSimpleName();
    private static final int BORDER_WIDTH = 12;
    private static final int BORDER_PAINT_WIDTH = 6;
    public static final int PADDING = 5;
    public static final int DEFAULT_INPUT_TYPE = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES |
            InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE;
    //    public static final int DEFAULT_INPUT_TYPE_WITHOUT_CORRECTION = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES |
//            InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
    public static final long TAP_PERIOD = 250L;
    private @ColorInt
    int textSelectedColor;
    private @ColorInt
    int highlightColor;
    private int strokeWidth;

    private @ColorInt
    int textBackgroundColor;
    private @ColorInt
    int textSelectionAreaColor;

    private int defaultWidth;

    private Context context;
    private TextView textBox;
    private TextStatus textStatus;
    private ViewGroup layout;
    private float totalDegree;
    private boolean edited;
    private String lastText;
    private float lastX;
    private float lastY;
    private int lastW;
    private int lastH;
    private float lastTextSize;
    private boolean lastTextScaled;
    private Deque<String> undoText;
    private Deque<Float> undoX;
    private Deque<Float> undoY;
    private Deque<Integer> undoW;
    private Deque<Integer> undoH;
    private Deque<Float> undoTextSize;
    private Deque<Boolean> undoTextScaled;
    private Deque<CharSequence> redoText;
    private Deque<Float> redoX;
    private Deque<Float> redoY;
    private Deque<Integer> redoW;
    private Deque<Integer> redoH;
    private Deque<Float> redoTextSize;
    private Deque<Boolean> redoTextScaled;
    private int lastCharacter;// the last character is the character user just typed, not the last character of text.
    private boolean justTouchDown;
    private int oldCursor;
    private int cursor;
    private int minHeight;
    private int scaleMinWidth;
    private boolean isTextScaling;
    private boolean hasFocus;
    private boolean selectionAreaVisible;
    private HitUnit hitUnit;
    private Rect rect;
    private RectF rectF;
    private Region regionClipIn, regionClipOut;
    private Region regionIn, regionOut;
    private RectF scalingRect;
    private float firstDragPaddingX, firstDragPaddingY;
    private LinearLayout.LayoutParams params;
    private boolean textScaled;
    private FontSizeCalculator fontSizeCalculator;

    public Word(Context context, float x, float y, @ColorInt int color, float size) {
        this.context = context;
        edited = false;
        lastCharacter = -1;
        lastText = "";
        undoText = new ArrayDeque<>();
        undoX = new ArrayDeque<>();
        undoY = new ArrayDeque<>();
        undoW = new ArrayDeque<>();
        undoH = new ArrayDeque<>();
        undoTextSize = new ArrayDeque<>();
        undoTextScaled = new ArrayDeque<>();
        redoText = new ArrayDeque<>();
        redoX = new ArrayDeque<>();
        redoY = new ArrayDeque<>();
        redoW = new ArrayDeque<>();
        redoH = new ArrayDeque<>();
        redoTextSize = new ArrayDeque<>();
        redoTextScaled = new ArrayDeque<>();
        justTouchDown = true;
        oldCursor = 0;
        cursor = 0;
        totalDegree = 0.0f;
        isTextScaling = false;
        selectionAreaVisible = false;
        rect = new Rect();
        rectF = new RectF();
        regionClipIn = new Region();
        regionClipOut = new Region();
        regionIn = new Region();
        regionOut = new Region();
        textScaled = false;

        float factor = DisplayDpiHelper.getInstance().getFake4kFactor();

        defaultWidth = context.getResources().getDimensionPixelSize(R.dimen.word_default_width);

        textBox = new androidx.appcompat.widget.AppCompatTextView(context);

        strokeWidth = (int) (context.getResources().getDimensionPixelSize(R.dimen.selection_box_thickness) * factor);

        int paddingStart = (int) (context.getResources().getDimensionPixelSize(R.dimen.word_text_box_padding_start) * factor);
        int paddingEnd = (int) (context.getResources().getDimensionPixelSize(R.dimen.word_text_box_padding_end) * factor);
        paddingEnd = (int) (context.getResources().getDimensionPixelSize(R.dimen.word_text_box_padding_end) * factor);
        int paddingTop = (int) (context.getResources().getDimensionPixelSize(R.dimen.word_text_box_padding_top) * factor);
        int paddingBottom = (int) (context.getResources().getDimensionPixelSize(R.dimen.word_text_box_padding_bottom) * factor);
        textBox.setInputType(DEFAULT_INPUT_TYPE);
        textBox.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
        textBox.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
        textBox.getPaint().setAntiAlias(true);
        textBox.setText("");
        minHeight = (int) TextLayoutUtil.getHeightOfString(textBox, "W");
        scaleMinWidth = (int) textBox.getPaint().measureText("W") + paddingStart + paddingEnd;
        textBox.setMinHeight(minHeight);

        textBox.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom);

        // set max width
        String hint = context.getString(R.string.hint_add_text_in_text_box);
        int paddingW = (int) ((paddingStart + paddingEnd) * factor);
        int hintWidth = (int) TextLayoutUtil.getWidthOfString(textBox, hint) + paddingW;
        int currentMaxWidth = textBox.getMaxWidth();
        int maxWidth = Math.min(currentMaxWidth, hintWidth);
        textBox.setMaxWidth(maxWidth);
        defaultWidth = Math.max(defaultWidth, maxWidth);

        textBox.setWidth(defaultWidth);

        textBox.measure(0, 0);
        textBox.setX(x);
        textBox.setY(y - textBox.getMeasuredHeight());

        // set true to enable focusable touch mode
        textBox.setFocusableInTouchMode(true);

        setPosition(textBox.getX(), textBox.getY());
        setSize(textBox.getMeasuredWidth(), textBox.getMeasuredHeight());

        textSelectedColor = ContextCompat.getColor(context, R.color.selection_highlight);
        highlightColor = ContextCompat.getColor(context, R.color.text_highlight_color);
        textBackgroundColor = ContextCompat.getColor(context, R.color.text_background_color);
        textSelectionAreaColor = ContextCompat.getColor(context, R.color.text_selection_Area_color);
        textBox.setHighlightColor(highlightColor);
        setColor(color);// set Text color here

//        textBox.setBackgroundResource(R.drawable.text_background);
        textBox.setBackgroundResource(factor > 1 ? R.drawable.text_background_rect_4k : R.drawable.text_background_rect);
        textBox.getBackground().setFilterBitmap(true);

        textBox.setGravity(Gravity.TOP | Gravity.START);

        layout = new LinearLayout(context);
        params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        textBox.setLayoutParams(params);
        layout.addView(textBox);

        hitUnit = new HitUnit();

        fontSizeCalculator = new FontSizeCalculator(textBox);

    }

    @Override
    public BaseItem copy() {
        Word word = new Word(context, getX(), getY(), getColor(), textBox.getTextSize());
        word.setEdited(true);
        word.setSelectionAreaVisible(this.selectionAreaVisible);
        word.setTextStatus(TextStatus.UNFOCUSED);
        textBox.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
        int width = textBox.getMeasuredWidth();
        int height = textBox.getMeasuredHeight();
        word.setText(textBox.getText().toString(), textBox.getX(), textBox.getY(),
                width, height, 0);

        word.setBounds(getX(), getY(), getWidth(), getHeight());
        PointF center = new PointF(getX() + getWidth() / 2.0f, getY() + getHeight() / 2.0f);
        word.rotateFromPoint(totalDegree, center);

        word.textScaled = this.textScaled;
        word.scaleMinWidth = this.scaleMinWidth;
        TextView textView = word.getTextView();
        textView.setMinWidth(textBox.getMinWidth());
        textView.setMaxWidth(textBox.getMaxWidth());

        return word;
    }

    public static BaseItem copyFromStateFile(Context context, float x, float y, float width, float height, int color, float textSize,
                                             float textBoxX, float textBoxY, int textBoxW, int textBoxH, int textBoxMinWidth, int textBoxMaxWidth,
                                             String textBoxString, float totalDegree, boolean textScaled, int scaleMinWidth) {
        Word word = new Word(context, x, y, color, textSize);
        word.setEdited(true);
        word.setTextStatus(TextStatus.UNFOCUSED);
        word.setText(textBoxString, textBoxX, textBoxY, textBoxW, textBoxH, 0);

        word.setBounds(x, y, width, height);
        PointF center = new PointF(x + width / 2.0f, y + height / 2.0f);
        word.rotateFromPoint(totalDegree, center);

        word.textScaled = textScaled;
        word.scaleMinWidth = scaleMinWidth;
        TextView textView = word.getTextView();
        textView.setMinWidth(textBoxMinWidth);
        textView.setMaxWidth(textBoxMaxWidth);

        return word;
    }

    public WordStateFile getWordStateFile() {
        textBox.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
        int width = textBox.getMeasuredWidth();
        int height = textBox.getMeasuredHeight();

        return new WordStateFile(this, textBox.getX(), textBox.getY(), width, height,
                textBox.getMinWidth(), textBox.getMaxWidth(), textBox.getText().toString(),
                totalDegree, textScaled, scaleMinWidth);
    }

    public void setCursor(MotionEvent event) {
        TextLayoutUtil.getCursorByEvent(textBox, event, true, new TextLayoutUtil.OngetCursorLis() {
            @Override
            public void onGetCursor(int cur) {
                cursor = cur;
            }
        });
    }

    public float getTextSize() {
        return textBox.getTextSize();
    }

    @Override
    public void setColor(int color) {
        super.setColor(color);
        if (textBox != null) {
            textBox.setTextColor(color);
        }
    }

    private void requestFocus() {
        quickSaveWordState();
        hasFocus = true;
        justTouchDown = true;
    }

    private void clearFocus() {
        hasFocus = false;
        justTouchDown = false;
    }

    @Override
    public void setSelected(boolean selected) {
        super.setSelected(selected);
        if (selected) {
            textStatus = TextStatus.SELECTED;
            textBox.setBackgroundResource(R.drawable.text_background);
            textBox.setTextColor(textSelectedColor);
        } else {
            textStatus = TextStatus.UNFOCUSED;
            textBox.setBackground(null);
            textBox.setTextColor(getColor());
        }
    }

    public boolean isSelectionAreaVisible() {
        return selectionAreaVisible;
    }

    public void setTextStatus(TextStatus status) {
        if (textStatus != null && textStatus.equals(status)) {
            return;
        }
        switch (status) {
            case UNFOCUSED: {
                enableWrapContent();
                setSelected(false);
                clearFocus();
                disableWrapContent();
                break;
            }
            case FOCUSED: {
                setSelected(false);
                requestFocus();
                break;
            }
            case SELECTED: {
                clearFocus();
                setSelected(true);
                break;
            }
            default: {
                log("TextStatus : Wrong status");
                break;
            }
        }
        textStatus = status;
    }

    private void enableWrapContent() {
        textBox.setMinWidth(scaleMinWidth);
    }

    private void disableWrapContent() {
        textBox.measure(0, 0);
        int width = textBox.getMeasuredWidth();
        int height = textBox.getMeasuredHeight();
        float oldCenterX = getX() + getWidth() / 2.0f;
        float oldCenterY = getY() + getHeight() / 2.0f;
        params.width = width;
        params.height = height;
        textBox.setLayoutParams(params);
//        textBox.setMinWidth(width);
        textBox.setWidth(width);

        if (totalDegree % 360.0f != 0.0f || textBox.getRotation() % 360.0f != 0.0f) {
            float centerX = textBox.getX() + textBox.getMeasuredWidth() / 2.0f;
            float centerY = textBox.getY() + textBox.getMeasuredHeight() / 2.0f;
            float centerOffsetX = centerX - oldCenterX;
            float centerOffsetY = centerY - oldCenterY;
            hitUnit.mat.reset();
            hitUnit.mat.setRotate(totalDegree, 0.0f, 0.0f);
            float[] offset = {centerOffsetX, centerOffsetY};
            hitUnit.mat.mapPoints(offset);
            centerOffsetX = offset[0];
            centerOffsetY = offset[1];

            float newCenterX = oldCenterX + centerOffsetX;
            float newCenterY = oldCenterY + centerOffsetY;
            textBox.measure(0, 0);
            float newTextX = newCenterX - textBox.getMeasuredWidth() / 2.0f;
            float newTextY = newCenterY - textBox.getMeasuredHeight() / 2.0f;

            textBox.setX(newTextX);
            textBox.setY(newTextY);
        }

        updateBound();
    }


    public TextStatus getTextStatus() {
        return textStatus;
    }

    public boolean isTextBoxBoundsChanged() {
        textBox.measure(0, 0);
//        return (lastX != textBox.getX() || lastY != textBox.getY() || lastW != textBox.getMeasuredWidth() || lastH != textBox.getMeasuredHeight());
        return (lastW != textBox.getMeasuredWidth() || lastH != textBox.getMeasuredHeight());
    }

    /** QuickSave the state of word as a temporary record. */
    public void quickSaveWordState() {
        textBox.measure(0, 0);
        lastW = textBox.getMeasuredWidth();
        lastH = textBox.getMeasuredHeight();
        lastX = textBox.getX();
        lastY = textBox.getY();
        lastText = textBox.getText().toString();
        lastTextSize = textBox.getTextSize();
        lastTextScaled = textScaled;
    }

    /** Save the state of word to be able to undo in future. */
    public void saveWordState() {
        undoW.push(lastW);
        undoH.push(lastH);
        undoX.push(lastX);
        undoY.push(lastY);
        undoText.push(lastText);
        undoTextSize.push(lastTextSize);
        undoTextScaled.push(lastTextScaled);
    }

    /** Set current word the state of word before previous action */
    public void loadWordState() {
        if (undoW.size() <= 0 || undoH.size() <= 0 || undoX.size() <= 0 || undoY.size() <= 0 || undoText.size() <= 0 || undoTextSize.size() <= 0
                || undoTextScaled.size() <= 0) {
            log("Error in loadWordState");
            return;
        }

        redoTextSize.push(textBox.getTextSize());
        textBox.setTextSize(TypedValue.COMPLEX_UNIT_PX, undoTextSize.pop());

        redoX.push(textBox.getX());
        textBox.setX(undoX.pop());

        redoY.push(textBox.getY());
        textBox.setY(undoY.pop());

        redoW.push(textBox.getWidth());
        textBox.setWidth(undoW.pop());

        redoTextScaled.push(textScaled);
        textScaled = undoTextScaled.pop();
        if (!textScaled) {
            textBox.setMaxWidth(defaultWidth);
        }

        redoH.push(textBox.getHeight());
        textBox.setHeight(undoH.pop());

        minHeight = (int) TextLayoutUtil.getHeightOfString(textBox, "W");
        textBox.setMinHeight(minHeight);

        redoText.push(textBox.getText());
        String text = undoText.pop();
        if (!textBox.getText().toString().equals(text)) {
            textBox.setText(text);
        }

        scaleMinWidth = (int) textBox.getPaint().measureText("W") + 2 * PADDING;

        updateTextLayout();

        updateBound();
    }

    public void redoWordState() {
        log("redoWordState");
        undoTextSize.push(textBox.getTextSize());
        textBox.setTextSize(TypedValue.COMPLEX_UNIT_PX, redoTextSize.pop());

        undoX.push(textBox.getX());
        textBox.setX(redoX.pop());

        undoY.push(textBox.getY());
        textBox.setY(redoY.pop());

        undoW.push(textBox.getWidth());
        textBox.setWidth(redoW.pop());

        undoTextScaled.push(textScaled);
        textScaled = redoTextScaled.pop();
        if (!textScaled) {
            textBox.setMaxWidth(defaultWidth);
        }

        undoH.push(textBox.getHeight());
        textBox.setHeight(redoH.pop());

        minHeight = (int) TextLayoutUtil.getHeightOfString(textBox, "W");
        textBox.setMinHeight(minHeight);

        undoText.push(textBox.getText().toString());
        CharSequence text = redoText.pop();
        if (!textBox.getText().equals(text)) {
            textBox.setText(text);
        }

        scaleMinWidth = (int) textBox.getPaint().measureText("W") + 2 * PADDING;

        updateTextLayout();

        updateBound();
    }

    private void updateTextLayout() {
        textBox.measure(0, 0);
        LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) textBox.getLayoutParams();
        layoutParams.width = textBox.getMeasuredWidth();
        layoutParams.height = textBox.getMeasuredHeight();
        textBox.setLayoutParams(layoutParams);
    }

    @Override
    public void moveBy(float dx, float dy) {
        super.moveBy(dx, dy);
        float centerX = getX() + getWidth() / 2;
        float centerY = getY() + getHeight() / 2;
        textBox.measure(0, 0);
        textBox.setX(centerX - textBox.getMeasuredWidth() / 2.0f);
        textBox.setY(centerY - textBox.getMeasuredHeight() / 2.0f);
        updateDetectRegion();
    }

    @Override
    public void scaleFromPoint(float rx, float ry, PointF p) {
        if (rx <= 0 || ry <= 0 || Double.isNaN(rx) || Double.isNaN(ry) || Double.isInfinite(rx) || Double.isInfinite(ry)) {
            return;
        }

        // Now the scaling ratio only depends on x-axis.
        if (rx == 1 || (textBox.getTextSize() >= WordProperty.MAX_SIZE && rx > 1) || (textBox.getTextSize() <= WordProperty.MIN_SIZE && rx < 1)) {
            return;
        }

        // TODO : Sometimes, the scaled text still does not fit the text box.

//        float boxRate = rx;

        fontSizeCalculator.calculateFontSizeAndRateToFitTextView(rx, WordProperty.MIN_SIZE, WordProperty.MAX_SIZE);

        float rate = fontSizeCalculator.getAdjustedRate();
        float newFontSize = fontSizeCalculator.getAdjustedFontSize();

        if (rate <=0 || newFontSize < WordProperty.MIN_SIZE || newFontSize > WordProperty.MAX_SIZE) {
            return;
        }

        float newX = (getX() - p.x) * rate + p.x;
        float newY = (getY() - p.y) * rate + p.y;
//        float newX = (getX() - p.x) * boxRate + p.x;
//        float newY = (getY() - p.y) * boxRate + p.y;
        float newW = getWidth() * rate;
        float newH = getHeight() * rate;

        setPosition(newX, newY);
        setSize(newW, newH);

        textBox.measure(0, 0);
        float txNewW = textBox.getMeasuredWidth() * rate;
        float txNewH = textBox.getMeasuredHeight() * rate;

        textBox.setTextSize(TypedValue.COMPLEX_UNIT_PX, newFontSize);

        textBox.setX(getX() + (getWidth() / 2) - (txNewW / 2));
        textBox.setY(getY() + (getHeight() / 2) - (txNewH / 2));
        textBox.setWidth(Math.round(txNewW));
        textBox.setHeight(Math.round(txNewH));

        this.minHeight = (int) TextLayoutUtil.getHeightOfString(textBox, "W");

        textScaled = true;
        textBox.setMinHeight(this.minHeight);
        textBox.setMaxHeight(Integer.MAX_VALUE);

        updateTextLayout();

        scaleMinWidth = (int) textBox.getPaint().measureText("W") + 2 * PADDING;

        /* The previous version of the scale function only changes the layout of the text box , it didn't scale the text. Hence, this function will need
         following codes to adjust the position of text box.
         Now the current scale function version does not make the text fit text box well, the width of layout still changes the layout and the allocation of
         text sometimes. I still need the following codes to adjust position in case the layout changes.*/
        if (totalDegree % 360.0f != 0.0f || textBox.getRotation() % 360.0f != 0.0f) {
            float boxCenterX = getX() + getWidth() / 2;
            float boxCenterY = getY() + getHeight() / 2;
            float centerOffsetX = (textBox.getX() + textBox.getMeasuredWidth() / 2.0f) - boxCenterX;
            float centerOffsetY = (textBox.getY() + textBox.getMeasuredHeight() / 2.0f) - boxCenterY;

            hitUnit.mat.reset();
            Matrix mat = hitUnit.mat;
            mat.setRotate(totalDegree, .0f, .0f);
            float[] pts = {centerOffsetX, centerOffsetY};
            mat.mapPoints(pts);
            float newCenterX = boxCenterX + pts[0];
            float newCenterY = boxCenterY + pts[1];
            float newTextX = newCenterX - textBox.getMeasuredWidth() / 2.0f;
            float newTextY = newCenterY - textBox.getMeasuredHeight() / 2.0f;
            textBox.setX(newTextX);
            textBox.setY(newTextY);
        }

        updateBound();

    }

    public void textScaleFromPoint(float dragX, float dragY) {
        // set textBox to the original width before scale preview
        textBox.setWidth((int) lastW);
        textBox.setMinHeight(this.minHeight);
        textBox.setMaxHeight(Integer.MAX_VALUE);

        textBox.measure(0, 0);
        float oldCenterX = getX() + getWidth() / 2.0f;
        float oldCenterY = getY() + getHeight() / 2.0f;

        if (totalDegree % 360.0f != 0.0f || textBox.getRotation() % 360.0f != 0.0f) {
//            float centerX = getX() + getWidth()/2.0f;
//            float centerY = getY() + getHeight()/2.0f;
            float[] pos = {dragX, dragY};
            hitUnit.mat.reset();
            hitUnit.mat.setRotate(-textBox.getRotation(), oldCenterX, oldCenterY);
            hitUnit.mat.mapPoints(pos);
            dragX = pos[0];
            dragY = pos[1];
        }

        float newWidth = dragX - textBox.getX() + firstDragPaddingX;

        if (newWidth < scaleMinWidth) {
            if (textBox.getMeasuredWidth() <= scaleMinWidth) {
                return;
            } else {
                newWidth = scaleMinWidth;
            }
        }

//        textBox.setWidth((int)newWidth);
        textBox.setWidth((int) Math.ceil(newWidth));
        enableWrapContent();
        textScaled = true;

        textBox.setMinHeight(this.minHeight);
        textBox.setMaxHeight(Integer.MAX_VALUE);

        updateTextLayout();

        if (totalDegree % 360.0f != 0.0f || textBox.getRotation() % 360.0f != 0.0f) {
//            textBox.measure(0,0);
            float centerX = textBox.getX() + textBox.getMeasuredWidth() / 2.0f;
            float centerY = textBox.getY() + textBox.getMeasuredHeight() / 2.0f;
            float centerOffsetX = centerX - oldCenterX;
            float centerOffsetY = centerY - oldCenterY;
            hitUnit.mat.reset();
            hitUnit.mat.setRotate(totalDegree, 0.0f, 0.0f);
            float[] offset = {centerOffsetX, centerOffsetY};
            hitUnit.mat.mapPoints(offset);
            centerOffsetX = offset[0];
            centerOffsetY = offset[1];

            float newCenterX = oldCenterX + centerOffsetX;
            float newCenterY = oldCenterY + centerOffsetY;
            textBox.measure(0, 0);
            float newTextX = newCenterX - textBox.getMeasuredWidth() / 2.0f;
            float newTextY = newCenterY - textBox.getMeasuredHeight() / 2.0f;

            textBox.setX(newTextX);
            textBox.setY(newTextY);
        }

        // resize
//        enableWrapContent();
//        disableWrapContent();

        // When there is at least an enter key (aka linebreak), the measured width of the TextView is not correctly calculated at first time.
        // U need to update the layout of the TextView twice to make the width be calculated correctly
        // I can not understand how can I solve this problem by updating the layout twice.
        updateTextLayout();

        updateBound();
    }

    public void prepareScalingTextBoxPreview() {
        if (scalingRect == null) {
            scalingRect = new RectF();
        }
        textBox.measure(0, 0);
        scalingRect.set(textBox.getX(), textBox.getY(), textBox.getX() + textBox.getMeasuredWidth(), textBox.getY() + textBox.getMeasuredHeight());
    }

    public void updateScalingTextBoxPreview(float dragX, float dragY) {
        if (scalingRect == null) {
            return;
        }

        if (!isTextScaling) {
            isTextScaling = true;
        }

        RectF r = scalingRect;
        float centerX = r.centerX();
        float centerY = r.centerY();

        if (totalDegree % 360.0f != 0.0f || textBox.getRotation() % 360.0f != 0.0f) {
            float[] pos = {dragX, dragY};
            hitUnit.mat.reset();
            hitUnit.mat.setRotate(-textBox.getRotation(), centerX, centerY);
            hitUnit.mat.mapPoints(pos);
            dragX = pos[0];
            dragY = pos[1];
        }

        float newWidth = dragX - r.left + firstDragPaddingX;

        if (newWidth < scaleMinWidth) {
            if (r.width() <= scaleMinWidth) {
                return;
            } else {
                newWidth = scaleMinWidth;
            }
        }

        r.right = r.left + newWidth;

        // get text layout height
//        float newHeight = TextLayoutUtil.getTextHeight((int)newWidth, textBox.getMeasuredHeight(), textBox);
//        r.bottom = r.top + newHeight;
        textBox.setWidth((int) newWidth);
        textBox.setMinHeight(this.minHeight);
        textBox.setMaxHeight(Integer.MAX_VALUE);
        textBox.measure(0, 0);
        r.bottom = r.top + textBox.getMeasuredHeight();

        float w = r.width();
        float h = r.height();

        if (totalDegree % 360.0f != 0.0f || textBox.getRotation() % 360.0f != 0.0f) {
            float centerOffsetX = r.centerX() - centerX;
            float centerOffsetY = r.centerY() - centerY;
            hitUnit.mat.reset();
            hitUnit.mat.setRotate(totalDegree, 0.0f, 0.0f);
            float[] offset = {centerOffsetX, centerOffsetY};
            hitUnit.mat.mapPoints(offset);
            centerOffsetX = offset[0];
            centerOffsetY = offset[1];

            float newCenterX = centerX + centerOffsetX;
            float newCenterY = centerY + centerOffsetY;
            float newX = newCenterX - r.width() / 2.0f;
            float newY = newCenterY - r.height() / 2.0f;

            r.set(newX, newY, newX + w, newY + h);
        }
    }

    public void setTextScaling(boolean b) {
        isTextScaling = b;
    }

    public void setText(String text, float x, float y, float w, float h, int cur) {
        quickSaveWordState();
        oldCursor = cursor;
        cursor = cur;

        textBox.setX(x);
        textBox.setY(y);
        textBox.setWidth((int) w);
        textBox.setHeight((int) h);
        textBox.setMinHeight(minHeight);
        textBox.setMaxHeight(Integer.MAX_VALUE);
        textBox.setText(text);

        updateTextLayout();

        updateBound();

        if (totalDegree % 360.0f == 0.0f) {
            setPosition(textBox.getX(), textBox.getY());
            setSize(Math.max(scaleMinWidth, w), h);
        }

    }

    @Override
    public void render(Canvas canvas, Paint paint, Context context) {
        if (!hasFocus) {
            if (selectionAreaVisible && getTextStatus().equals(TextStatus.UNFOCUSED)) {
                canvas.save();

                Paint.Style tempStyle = paint.getStyle();
                @ColorInt int tempColor = paint.getColor();
                float tempStrokeWidth = paint.getStrokeWidth();

                canvas.rotate(totalDegree, getX() + getWidth() / 2.0f, getY() + getHeight() / 2.0f);
                paint.setStyle(Paint.Style.STROKE);
                paint.setColor(textSelectionAreaColor);
                paint.setStrokeWidth(BORDER_PAINT_WIDTH);
                textBox.measure(0, 0);
                canvas.drawRect(textBox.getX() - BORDER_WIDTH / 2.0f, textBox.getY() - BORDER_WIDTH / 2.0f,
                        textBox.getX() + textBox.getMeasuredWidth() + BORDER_WIDTH / 2.0f,
                        textBox.getY() + textBox.getMeasuredHeight() + BORDER_WIDTH / 2.0f, paint);

                paint.setStyle(tempStyle);
                paint.setColor(tempColor);
                paint.setStrokeWidth(tempStrokeWidth);
                canvas.restore();
            }

            if (isTextScaling && scalingRect != null) {
                canvas.save();
                Paint.Style tempStyle = paint.getStyle();
                @ColorInt int tempColor = paint.getColor();
                int tempAlpha = paint.getAlpha();

                canvas.rotate(totalDegree, scalingRect.centerX(), scalingRect.centerY());
                paint.setStyle(Paint.Style.FILL);
                paint.setColor(textSelectedColor);
                paint.setAlpha(0x4f);

                canvas.drawRect(scalingRect, paint);

                paint.setStyle(tempStyle);
                paint.setColor(tempColor);
                paint.setAlpha(tempAlpha);
                canvas.restore();
            } else {
                layout.measure(View.MeasureSpec.makeMeasureSpec(canvas.getWidth(), View.MeasureSpec.EXACTLY),
                        View.MeasureSpec.makeMeasureSpec(canvas.getHeight(), View.MeasureSpec.EXACTLY));
                layout.layout(0, 0, canvas.getWidth(), canvas.getHeight());
                layout.draw(canvas);
            }
        }

    }

    @Override
    public void rotateFromPoint(float d, PointF p) {
        this.totalDegree += d;

        float centerX = getX() + getWidth() / 2;
        float centerY = getY() + getHeight() / 2;
        hitUnit.mat.reset();
        Matrix mat = hitUnit.mat;
        mat.setRotate(d, p.x, p.y);
        float[] pts = {centerX, centerY};
        mat.mapPoints(pts);
        this.moveBy(pts[0] - centerX, pts[1] - centerY);
        updateBound();
    }

    // update boundary of selection box after u have set up the position and size of editText.
    private void updateBound() {
        textBox.measure(0, 0);
        float centerX = textBox.getX() + textBox.getMeasuredWidth() / 2.0f;
        float centerY = textBox.getY() + textBox.getMeasuredHeight() / 2.0f;
        float oriW, oriH, newW, newH;
        float newX, newY;
        float radian = (float) Math.toRadians(totalDegree);
        float cos = Math.abs(((float) Math.cos(radian)));
        float sin = Math.abs(((float) Math.sin(radian)));

        oriW = textBox.getMeasuredWidth();
        oriH = textBox.getMeasuredHeight();
        newW = sin * oriH + cos * oriW;
        newH = cos * oriH + sin * oriW;
        newX = centerX - newW / 2;
        newY = centerY - newH / 2;

        setPosition(newX, newY);
        setSize(newW, newH);

        textBox.setRotation(totalDegree);
        textBox.invalidate();
        textBox.requestLayout();
    }

    public void updateDetectRegion() {
        textBox.measure(0, 0);
        float left = textBox.getX();
        float right = textBox.getX() + textBox.getMeasuredWidth();
        float top = textBox.getY();
        float bottom = textBox.getY() + textBox.getMeasuredHeight();

        rectF.set(getX(), getY(), getX() + getWidth(), getY() + getHeight());
        rectF.roundOut(rect);
        regionClipIn.set(rect);
        setRectDetectRegion(regionIn, regionClipIn, left, top, right, bottom);

//        rectF.set(getX() - BORDER_WIDTH, getY() - BORDER_WIDTH, getX() + getWidth() + BORDER_WIDTH, getY() + getHeight() + BORDER_WIDTH);
//        rectF.roundOut(rect);
//        regionClipOut.set(rect);
//        setRectDetectRegion(regionOut, regionClipOut, left - BORDER_WIDTH, top - BORDER_WIDTH, right + BORDER_WIDTH, bottom + BORDER_WIDTH);

    }

    private void setRectDetectRegion(Region region, Region clipRegion, float left, float top, float right, float bottom) {
        textBox.measure(0, 0);
        float centerX = getX() + getWidth() / 2.0f;
        float centerY = getY() + getHeight() / 2.0f;

        float[] posLT = {left, top};
        float[] posRT = {right, top};
        float[] posRB = {right, bottom};
        float[] posLB = {left, bottom};

        if (totalDegree % 360.0f != 0.0f || textBox.getRotation() % 360.0f != 0.0f) {
            hitUnit.mat.reset();
            hitUnit.mat.setRotate(totalDegree, centerX, centerY);
            hitUnit.mat.mapPoints(posLT);
            hitUnit.mat.mapPoints(posRT);
            hitUnit.mat.mapPoints(posRB);
            hitUnit.mat.mapPoints(posLB);
        }

        hitUnit.path.rewind();

        hitUnit.path.moveTo(posLT[0], posLT[1]);
        hitUnit.path.lineTo(posRT[0], posRT[1]);
        hitUnit.path.lineTo(posRB[0], posRB[1]);
        hitUnit.path.lineTo(posLB[0], posLB[1]);

        region.setPath(hitUnit.path, clipRegion);
    }

    public float[] getTextBoxBounds() {
        textBox.measure(0, 0);
        return new float[] {textBox.getX(), textBox.getY(), textBox.getX() + textBox.getMeasuredWidth(), textBox.getY() + textBox.getMeasuredHeight()};
    }

    public void setFirstDragPadding(float[] padding) {
        firstDragPaddingX = padding[0];
        firstDragPaddingY = padding[1];
    }

    public float getRotation() {
        return textBox.getRotation();
    }

    private double pointsDistance(PointF A, PointF B) {
        return Math.sqrt((Math.pow(A.x - B.x, 2) + Math.pow(A.y - B.y, 2)));
    }

    private double mathGetArea(PointF A, PointF B, PointF C) {
        double a = pointsDistance(A, B);
        double b = pointsDistance(B, C);
        double c = pointsDistance(C, A);
        double s = (a + b + c) / 2;
        return Math.sqrt(s * (s - a) * (s - b) * (s - c));
    }

    public boolean hitBox(float x, float y) {
        return (x >= getX() && x <= getX() + getWidth() && y >= getY() && y <= getY() + getHeight());
    }

    public boolean hitArea(float x, float y) {//check if (x,y) is inside the textarea/quadrangle or not
        updateDetectRegion();
//        float X = textBox.getX();
//        float Y = textBox.getY();
//        textBox.measure(0,0);
//        float W = textBox.getMeasuredWidth();
//        float H = textBox.getMeasuredHeight();
//
//        return hitRectangle(x, y, X, Y, W, H);

        return hitRectangle(regionIn, (int) x, (int) y);
    }

    public boolean hitBorder(float x, float y) {
////        boolean out = ( x>=getX()-BORDER_WIDTH && x <= getX()+ getWidth()+BORDER_WIDTH
////                && y>=getY()-BORDER_WIDTH && y <= getY()+getHeight()+BORDER_WIDTH );
////        boolean in = ( x>=getX() && x <= getX()+ getWidth()
////                && y>=getY() && y <= getY()+getHeight() );
//
//        float textBoxWidth = textBox.getWidth();
//        float textBoxHeight = textBox.getHeight();
//
//        if (textBoxWidth==0 || textBoxHeight==0) {
//            textBox.measure(0,0);
//            textBoxWidth = textBox.getMeasuredWidth();
//            textBoxHeight = textBox.getMeasuredHeight();
//        }
//
//        boolean out = hitRectangle(x, y, textBox.getX()-BORDER_WIDTH, textBox.getY()-BORDER_WIDTH,
//                textBoxWidth+2*BORDER_WIDTH, textBoxHeight+2*BORDER_WIDTH);
//        boolean in = hitRectangle(x, y, textBox.getX(), textBox.getY(), textBoxWidth, textBoxHeight);
//
        boolean out = hitRectangle(regionOut, (int) x, (int) y);
        boolean in = hitRectangle(regionIn, (int) x, (int) y);

        return out && !in;
    }

    private class HitUnit {
        PointF pos;
        PointF center;
        Matrix mat;
        PointF LB;
        PointF RB;
        PointF RT;
        PointF LT;
        Path path;

        HitUnit() {
            pos = new PointF();
            center = new PointF();
            mat = new Matrix();
            LB = new PointF();
            RB = new PointF();
            RT = new PointF();
            LT = new PointF();
            path = new Path();
        }
    }

    @Override
    public void getRegion(Region out) {
//        super.getRegion(out);
        updateDetectRegion();
        out.set(regionIn);
    }

    private boolean hitRectangle(Region region, int x, int y) {
        return region != null && region.contains(x, y);
    }

    private boolean hitRectangle(float touchX, float touchY, float X, float Y, float W, float H) {//check if (x,y) is inside the textarea/quadrangle or not
        hitUnit.pos.set(touchX, touchY);
        hitUnit.center.set(X + W / 2, Y + H / 2);
        hitUnit.mat.reset();
        PointF pos = hitUnit.pos;
        PointF center = hitUnit.center;
        Matrix mat = hitUnit.mat;

        mat.setRotate(totalDegree, center.x, center.y);

        float lb[] = {X, Y};
        mat.mapPoints(lb);
        hitUnit.LB.set(lb[0], lb[1]);
        PointF LB = hitUnit.LB;
        float rb[] = {X + W, Y};
        mat.mapPoints(rb);
        hitUnit.RB.set(rb[0], rb[1]);
        PointF RB = hitUnit.RB;
        float rt[] = {X + W, Y + H};
        mat.mapPoints(rt);
        hitUnit.RT.set(rt[0], rt[1]);
        PointF RT = hitUnit.RT;
        float lt[] = {X, Y + H};
        mat.mapPoints(lt);
        hitUnit.LT.set(lt[0], lt[1]);
        PointF LT = hitUnit.LT;

        double A = mathGetArea(LT, LB, pos);
        double B = mathGetArea(LB, RB, pos);
        double C = mathGetArea(RB, RT, pos);
        double D = mathGetArea(RT, LT, pos);

        return ((A + B + C + D) <= W * H * 1.00001);
    }

    public int getCursor() {
        return cursor;
    }

    public int getOldCursor() {
        return oldCursor;
    }

    public void setJustTouchDown(boolean b) {
        justTouchDown = b;
    }

    public boolean getJustTouchDown() {
        return justTouchDown;
    }

    public String getLastText() {
        return lastText;
    }

    public String getText() {
        return textBox.getText().toString();
    }

    public void setLastCharacter(int c) {
        lastCharacter = c;
    }

    public int getLastCharacter() {
        return lastCharacter;
    }

    public boolean getEdited() {
        return edited;
    }

    public void setEdited(boolean b) {
        if (b) {
            textBox.setMinHeight(minHeight);
        }
        edited = b;
    }

    public void setSelectionAreaVisible(boolean selectionAreaVisible) {
        this.selectionAreaVisible = selectionAreaVisible;
    }

    public void setMinWidth(int minWidth) {
        this.textBox.setMinWidth(minWidth);
    }

    private TextView getTextView() {
        return this.textBox;
    }

    private void log(String str) {
        Log.d(TAG, "(Yuda)" + str);
    }

    public enum TextStatus {
        UNFOCUSED, FOCUSED, SELECTED
    }

    public Config getConfig() {
        final Config config = new Config();

        config.x = textBox.getX();
        config.y = textBox.getY();
        textBox.measure(0, 0);
        config.width = textBox.getMeasuredWidth();
        config.maxWidth = textBox.getMaxWidth();
        config.minWidth = textBox.getMinWidth();
        config.height = textBox.getMeasuredHeight();
        config.minHeight = this.minHeight;
        config.rotation = textBox.getRotation();
        config.text = textBox.getText().toString();
        config.textSize = textBox.getTextSize();
        config.textColor = textBox.getCurrentTextColor();
        config.boundX = this.getX();
        config.boundY = this.getY();
        config.boundW = this.getWidth();
        config.boundH = this.getHeight();
        config.edited = this.getEdited();
        config.layoutWidth = textBox.getMeasuredWidth();
        config.layoutHeight = textBox.getMeasuredHeight();
        config.textScaled = this.textScaled;

        return config;
    }

    public static class Config {
        private float x;
        private float y;
        private int width;
        private int maxWidth;
        private int minWidth;
        private int height;
        private int minHeight;
        private float rotation;
        private float boundX;
        private float boundY;
        private float boundW;
        private float boundH;
        private boolean edited;
        private int layoutHeight;
        private int layoutWidth;
        private boolean textScaled;

        private String text;
        private float textSize;
        @ColorInt
        private int textColor;

        public float getX() {
            return x;
        }

        public float getY() {
            return y;
        }

        public int getWidth() {
            return width;
        }

        public int getMaxWidth() {
            return maxWidth;
        }

        public int getMinWidth() {
            return minWidth;
        }

        public int getHeight() {
            return height;
        }

        public int getMinHeight() {
            return minHeight;
        }

        public float getRotation() {
            return rotation;
        }

        public String getText() {
            return text;
        }

        public float getTextSize() {
            return textSize;
        }

        @ColorInt
        public int getTextColor() {
            return textColor;
        }

        @Override
        public String toString() {
            return "Config: x=" + x + ", y=" + y + ", width=" + width +
                    ", maxWidth=" + maxWidth + ", minWidth=" + minWidth + ", height=" + height +
                    ", minHeight=" + minHeight + ", rotation=" + rotation + ", text=" + text +
                    ", textSize=" + textSize + ", textColor=" + textColor;
        }

        public float getBoundX() {
            return boundX;
        }

        public float getBoundY() {
            return boundY;
        }

        public float getBoundW() {
            return boundW;
        }

        public float getBoundH() {
            return boundH;
        }

        public boolean getEdited() {
            return edited;
        }

        public int getLayoutHeight() {
            return layoutHeight;
        }

        public int getLayoutWidth() {
            return layoutWidth;
        }

        public boolean isTextScaled() {
            return textScaled;
        }
    }
}
